﻿#define BYTESONLY

using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using O1.Kernel.Storage;

namespace Tests.O1.Kernel.Storage
{
    [TestClass]
    public class BlockArrayTests
    {
        private const int TestBlockSize = BlockArray<byte[]>.BlockSize;

        private static byte[][] randomData = BuildRandomData();

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanAllocate0Size()
        {
            Action test = () => new BlockArray<byte[]>(0);
            test.ShouldNotThrow();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanAllocate0SizeAmatorizeJIT()
        {
            Action test = () => new BlockArray<byte[]>(0);
            test.ShouldNotThrow();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanAllocateBlockSizeExactly()
        {
            Action test = () => new BlockArray<byte[]>(TestBlockSize);
            test.ShouldNotThrow();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayLengthMatches0Size()
        {
            var block = new BlockArray<byte[]>(0);
            block.Length.Should().Be(0);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayLengthMatchesExactBlockSize()
        {
            var block = new BlockArray<byte[]>(TestBlockSize * 2);
            block.Length.Should().Be(TestBlockSize * 2);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArraySetsAndGetsDataAt3BlocksWithReferenceAssertions()
        {
            var block = new BlockArray<byte[]>(randomData.Length);
            for (var i = 0; i < randomData.Length; i++)
            {
                block[i] = randomData[i];
            }

            for (var i = 0; i < randomData.Length; i++)
            {
                block[i].Should().BeSameAs(randomData[i]);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArraySetsAndGetsDataAt3BlocksWithoutAssertions()
        {
            var block = new BlockArray<byte[]>(randomData.Length);
            for (var i = 0; i < randomData.Length; i++)
            {
                block[i] = randomData[i];
            }

            for (var i = 0; i < randomData.Length; i++)
            {
                block[i] = randomData[i];
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArraySetsAndGetsDataAtSingleExecutionWithoutAssertions()
        {
            var block = new BlockArray<byte[]>(randomData.Length);
            block[TestBlockSize - 1] = randomData[TestBlockSize - 1];
            block[TestBlockSize - 1].Should().NotBeNull();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayDirtyReadDataAt3BlocksWithoutAssertions()
        {
            var block = new BlockArray<byte[]>(randomData.Length);
            for (var i = 0; i < randomData.Length; i++)
            {
                block[i] = randomData[i];
            }

            for (var i = 0; i < randomData.Length; i++)
            {
                block[i].Should().BeSameAs(randomData[i]);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandThrowsArgumentOutOfRangeExceptionWhenNewSizeIs0()
        {
            var block = new BlockArray<byte[]>(1000);
            block.Length.Should().Be(1000);

            Action test = () => block.ExpandToIndex(0);
            test.ShouldThrow<ArgumentOutOfRangeException>();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandThrowsArgumentOutOfRangeExceptionWhenNewSizeIsNegative()
        {
            var block = new BlockArray<byte[]>(1000);
            block.Length.Should().Be(1000);

            Action test = () => block.ExpandToIndex(-100);
            test.ShouldThrow<ArgumentOutOfRangeException>();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandDoesNothingWhenNewSizeDoesNotIncrease()
        {
            var block = new BlockArray<byte[]>(1000);
            block.Length.Should().Be(1000);
            block.ExpandToIndex(10);
            block.Length.Should().Be(1000);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandDoesNothingWhenNewSizeIsEqual()
        {
            var block = new BlockArray<byte[]>(1000);
            block.Length.Should().Be(1000);
            block.ExpandToIndex(1000);
            block.Length.Should().Be(1001);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandIncreasesSizeWhenNewSizeIsGreaterButStillOneBlock()
        {
            var block = new BlockArray<byte[]>(1000);
            block.Length.Should().Be(1000);
            block.ExpandToIndex(2000);
            block.Length.Should().Be(2001);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayExpandIncreasesSizeWhenNewSizeIsGreater()
        {
            var block = new BlockArray<byte[]>(1000 + TestBlockSize);
            block.Length.Should().Be(1000 + TestBlockSize);
            block.ExpandToIndex(2000 + TestBlockSize);
            block.Length.Should().Be(2001 + TestBlockSize);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanSetAtInitialLength()
        {
            var block = new BlockArray<byte[]>(1000);
            block.ExpandToIndex(1000);
            block.Length.Should().Be(1001);
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanExpandAtExactBlockSizeAfterInitialCapacityOf0()
        {
            // Check the next capacity allocation:
            var blockSize = BlockArray<byte[]>.BlockSize * 2;
            
            var block = new BlockArray<byte[]>(0);
            block.ExpandToIndex(blockSize);
            Action test = () => block[blockSize] = new byte[0];
            test.ShouldNotThrow();
        }

        [TestMethod, TestCategory("Unit Test")]
        public void BlockArrayCanExpandAtExactChunkSizeAfterInitialCapacityOf2Blocks()
        {
            // Check the next capacity allocation:
            var blockSize = BlockArray<byte[]>.BlockSize * 2;

            var block = new BlockArray<byte[]>(blockSize);
            block.ExpandToIndex(blockSize);
            Action test = () => block[blockSize] = new byte[0];
            test.ShouldNotThrow();
        }

        private static byte[][] BuildRandomData()
        {
            var rnd = new Random();
            var data = new byte[TestBlockSize + 99][];
            for (var i = 0; i < data.Length; i++)
            {
                var buffer = new byte[4];
                rnd.NextBytes(buffer);
                data[i] = buffer;
            }

            return data;
        }
    }
}
